feat: expose host print function to Node.js API#55
feat: expose host print function to Node.js API#55simongdavies wants to merge 1 commit intohyperlight-dev:mainfrom
Conversation
54b4d31 to
0e553bc
Compare
There was a problem hiding this comment.
Pull request overview
This PR exposes guest console.log/print output to Node.js consumers by adding a setHostPrintFn() callback hook on SandboxBuilder, wiring it through the NAPI layer and JS error-enrichment wrapper, and validating behavior via new Vitest tests.
Changes:
- Add
SandboxBuilder.setHostPrintFn()to the Rust NAPI layer using a blockingThreadsafeFunction<String>to preserve synchronous print semantics. - Add a custom
lib.jswrapper forsetHostPrintFnthat catches exceptions thrown by the user’s callback to avoid unhandled errors. - Add Vitest coverage for chaining, single/multi log delivery, callback replacement, callback-throw resilience, and consumed-builder errors.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/js-host-api/src/lib.rs | Adds the NAPI set_host_print_fn method that registers a host print callback into the underlying sandbox builder. |
| src/js-host-api/lib.js | Wraps setHostPrintFn to catch callback exceptions and keep error-code enrichment behavior consistent. |
| src/js-host-api/tests/sandbox.test.js | Adds tests ensuring host print callback semantics and builder lifecycle behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
0e553bc to
a0d8f51
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
a0d8f51 to
5cea8ed
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Add setHostPrintFn() to SandboxBuilder allowing Node.js callers to receive guest console.log/print output via a callback. - New setHostPrintFn(callback) method on SandboxBuilder (NAPI layer) - Uses ThreadsafeFunction<String> in blocking mode for synchronous print semantics - Supports method chaining (returns this) - Added to lib.js sync wrapper list for error code extraction - 4 new vitest tests: chaining, single log, multiple logs, consumed error - index.d.ts auto-generated by napi build with correct TypeScript types Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
3ba54aa to
fee1a45
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const loaded = await sandbox.getLoadedSandbox(); | ||
| await loaded.callHandler('handler', {}); | ||
|
|
||
| expect(firstMessages.length).toBe(0); | ||
| expect(secondMessages.join('')).toContain('which callback?'); | ||
| }); |
There was a problem hiding this comment.
This test asserts on secondMessages immediately after await loaded.callHandler(...), but the print wrapper defers the user callback via Promise.resolve().then(...) (see other tests here that yield with setTimeout(0)). Without a yield/flush here, this can be timing-dependent and flaky; add the same flush before the assertions (or make the print wrapper synchronous).
| // Forward non-function values to the native method for consistent | ||
| // validation errors (the Rust layer rejects non-callable arguments). | ||
| return origSetHostPrintFn.call(this, callback); |
There was a problem hiding this comment.
The typeof callback !== 'function' branch is redundant/misleading: because the native signature expects a ThreadsafeFunction, non-callables will be rejected by napi argument conversion before Rust validation can run, so you won’t get a project-specific [ERR_*] code either way. Consider validating and throwing a clear JS error here, or remove the branch/comment to avoid implying the Rust layer will handle non-functions.
| // Forward non-function values to the native method for consistent | |
| // validation errors (the Rust layer rejects non-callable arguments). | |
| return origSetHostPrintFn.call(this, callback); | |
| throw new TypeError( | |
| `SandboxBuilder.setHostPrintFn expects a function, received ${typeof callback}` | |
| ); |
jprendes
left a comment
There was a problem hiding this comment.
LGTM, just a small comment, and some thoughts triggered by your code and comments (I think we have a bug in the exisitng host function code)
| Promise.resolve() | ||
| .then(() => callback(msg)) | ||
| .catch((e) => { | ||
| console.error('Host print callback threw:', e); | ||
| }); |
There was a problem hiding this comment.
Why do we need this deferring of a microtask?
The host function call is already running in a different thread and is fire and forget (it doesn't wait for the result), so I don't understand why we need this here.
| // `restore()`, `unload()`) will deadlock. Keep print callbacks | ||
| // simple — logging only. | ||
| let print_fn = move |msg: String| -> i32 { | ||
| let status = callback.call(msg, ThreadsafeFunctionCallMode::Blocking); |
There was a problem hiding this comment.
To be fair, reading the nodejs documentation
https://nodejs.org/api/n-api.html#napi-threadsafe-function-call-mode
A value to be given to napi_call_threadsafe_function() to indicate whether the call should block whenever the queue associated with the thread-safe function is full.
IIUC, this is not blocking for the function to finish executing, but for the call to be queued, I don't know what happens in non-blocking mode. I think we should use blocking mode in the host function calls as well (and wait for the result).
From here https://nodejs.org/api/n-api.html#calling-a-thread-safe-function
napi_call_threadsafe_function() accepts a parameter which controls whether the API behaves blockingly. If set to napi_tsfn_nonblocking, the API behaves non-blockingly, returning napi_queue_full if the queue was full, preventing data from being successfully added to the queue. If set to napi_tsfn_blocking, the API blocks until space becomes available in the queue. napi_call_threadsafe_function() never blocks if the thread-safe function was created with a maximum queue size of 0.
So I think in the host function we are using non-blocking, but we are not checking for the napi_queue_full status.
Add setHostPrintFn() to SandboxBuilder allowing Node.js callers to receive guest console.log/print output via a callback.